package com.elnguage.lox;

import com.elnguage.lox.Expr.Binary;
import com.elnguage.lox.Expr.EventCat;
import com.elnguage.lox.Expr.Grouping;
import com.elnguage.lox.Expr.Unary;
import com.elnguage.lox.Stmt.Expression;
import com.elnguage.lox.Stmt.Print;
import com.elnguage.lox.BaseStmt.BasicStmt;
import com.elnguage.lox.BaseStmt.StellaStmt;
import com.elnguage.lox.BaseStmt;
import com.elnguage.lox.StellarisStmt;
import com.elnguage.lox.StellarisStmt.Block;
import com.elnguage.lox.StellarisStmt.Call;
import com.elnguage.lox.StellarisStmt.Event;
import com.elnguage.lox.StellarisStmt.Handle;
import com.elnguage.lox.LoxFunction;
import java.util.HashMap;
import java.util.Map;

import static com.elnguage.lox.TokenType.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;

import java.util.List;

public class Interpreter implements Expr.Visitor<Object>, Stmt.Visitor<Void>, BaseStmt.Visitor<Void>, StellarisStmt.Visitor<Void>
{
    private Interpreter interpreter;
    final Environment globals = new StellaGlobalEnvironment();
    private Environment environment = globals;
    Environment currentEnv = globals;
    Interpreter() {
        interpreter = this;
        globals.define("clock", new LoxCallable() {
          @Override
          public int arity() { return 0; }
    
          @Override
          public Object call(Interpreter interpreter,
                             List<Object> arguments) {
            return (double)System.currentTimeMillis() / 1000.0;
          }
    
          @Override
          public String toString() { return "<native fn> clock"; }
        });
        //Add built-in tick
        globals.define("tick", new LoxCallable() {
            @Override
            public int arity() { return 0; }

          @Override
          public Object call(Interpreter interpreter,
                             List<Object> arguments) {
            StellaGlobalEnvironment s_globals = (StellaGlobalEnvironment) globals;
            s_globals.SetGlobalTick(s_globals.GetGlobalTick() + 1);
            for(Environment env : s_globals.tick_regist_event.keySet())
            {
                for(StellaEvent event : s_globals.tick_regist_event.get(env))
                {
                    event.call(interpreter, null);
                }
            }
            return null;
          }
    
          @Override
          public String toString() { return "<native fn> tick"; }
        });

        //Add built-in visit
        globals.define("visit", new LoxCallable() {

            @Override
            public int arity() {
                return 1;
            }

            @Override
            public Object call(Interpreter interpreter, List<Object> arguments) {
                if(arguments.get(0).toString().equals("global"))
                {
                    currentEnv = globals;
                    return null;
                }
                StellaGlobalEnvironment s_globals = (StellaGlobalEnvironment)globals;
                Environment env = s_globals.GetEnvByName((String)arguments.get(0));
                currentEnv = env;
                if(env == null)
                {
                    System.err.println("Not a visible scope");
                    return null;
                }
                return null;
            }
        });

        //add built-in register tick
        globals.define("register", new LoxCallable() {

            @Override
            public int arity() {
                return 2;
            }

            @Override
            public Object call(Interpreter interpreter, List<Object> arguments) {
                String target_name = arguments.get(0).toString();
                String event_name = arguments.get(1).toString();
                StellaGlobalEnvironment s_globals = (StellaGlobalEnvironment)globals;
                Environment target_env = s_globals.GetEnvByName(target_name);
                StellaEvent event = (StellaEvent)globals.values.get(event_name);
                ((StellaGlobalEnvironment)globals).tick_regist_event.get(target_env).add(event);
                //System.out.println(((StellaGlobalEnvironment)globals).tick_regist_event);
                return null;
            }
        });
        
        //Add built-in printCountry
        globals.define("ls_country", new LoxCallable() {
            @Override
            public int arity() {
                return 0;
            }

            @Override
            public Object call(Interpreter interpreter, List<Object> arguments) {
                for (StellaCountryEnvironment stellaCountryEnvironment : ((StellaGlobalEnvironment)(globals)).country_list) {
                    System.out.println(stellaCountryEnvironment.values.get("name"));
                  }
                return null;
            }
        });

        //Add built-in printPlanet
        globals.define("ls_planet", new LoxCallable() {
            @Override
            public int arity() {
                return 0;
            }

            @Override
            public Object call(Interpreter interpreter, List<Object> arguments) {
                for (StellaPlanetEnvironment stellaPlanetEnvironment : ((StellaGlobalEnvironment)(globals)).planet_list) {
                    System.out.println(stellaPlanetEnvironment.values.get("name"));
                  }
                return null;
            }
        });
      }
    public Environment GetCurrentEnv()
    {
        return currentEnv;
    }
    public void interpret(List<BaseStmt> statements)
    {
        try
        {
            for(BaseStmt statement : statements)
            {
                execute(statement);
            }
        }
        catch(RuntimeError error)
        {
            Lox.runtimeError(error);
        }
    }
    private void execute(BaseStmt statement)
    {
        statement.accept(this);
    }

    public void executeBlock(List<Stmt> statements, Environment environment)
    {
        Environment previous = this.environment;
        try {
            this.environment = environment;
            for (Stmt statement : statements) {
                execute(new BaseStmt.BasicStmt(statement));
            }
        } 
        finally 
        {
            this.environment = previous;
        }
    }

    @Override
    public Void visitBlockStmt(Stmt.Block stmt) {
      executeBlock(stmt.statements, new Environment(environment));
      return null;
    }

    private Object evaluate(Expr expr)
    {
        return expr.accept(this);
    }
    @Override
    public Object visitLiteralExpr(Expr.Literal expr)
    {
        return expr.value;
    }

    @Override
    public Object visitLogicalExpr(Expr.Logical expr) {
      Object left = evaluate(expr.left);
  
      if (expr.operator.type == TokenType.OR) {
        if (isTrue(left)) return left;
      } else {
        if (!isTrue(left)) return left;
      }
  
      return evaluate(expr.right);
    }

    @Override
    public Object visitBinaryExpr(Binary expr) {
        var left = evaluate(expr.left);
        var right = evaluate(expr.right);
        switch(expr.operator.type)
        {
            case PLUS:
                if(left instanceof Double && right instanceof Double)
                    return ((double)left + (double)right);
                if(left instanceof Integer && right instanceof Double)
                    return (Double.valueOf((int)left) + (double)right);
                if(right instanceof Integer && left instanceof Double)
                    return (Double.valueOf((int)right) + (double)left);
                if(left instanceof String && right instanceof String)
                    return ((String)left + (String)right);
                throw new RuntimeError(expr.operator, "Operands must be numbers or strings.");
            case MINUS:
                checkNumOperands(expr.operator, left, right);
                return ((double)left - (double)right);
            case SLASH:
                checkNumOperands(expr.operator, left, right);
                return ((double)left / (double)right);
            case STAR:
                checkNumOperands(expr.operator, left, right);
                return ((double)left * (double)right);

            //comparison
            case GREATER:
                checkNumOperands(expr.operator, left, right);
                return ((double)left) > ((double)right);
            case GREATER_EQUAL:
                checkNumOperands(expr.operator, left, right);
                return ((double)left) >= ((double)right);
            case LESS:
                checkNumOperands(expr.operator, left, right);
                return ((double)left) < ((double)right);
            case LESS_EQUAL:
                checkNumOperands(expr.operator, left, right);
                return ((double)left) <= ((double)right);
            case EQUAL_EQUAL:
                if(left == null && right == null)
                    return true;
                else if(left == null || right == null)
                    return false;
                //Exceptions may happend?
                return left.equals(right);
            case BANG_EQUAL:
                return (left) != (right);
            default:

        }

        //unreachable
        return null;
    }

    @Override
    public Object visitCallExpr(Expr.Call expr) {
      Object callee = evaluate(expr.callee);
  
      List<Object> arguments = new ArrayList<>();
      for (Expr argument : expr.arguments) { 
        arguments.add(evaluate(argument));
      }
      
      if (!(callee instanceof LoxCallable)) {
        throw new RuntimeError(expr.paren,
            "Can only call functions and classes.");
      }

      LoxCallable function = (LoxCallable)callee;

      if (arguments.size() != function.arity()) {
        throw new RuntimeError(expr.paren, "Expected " +
            function.arity() + " arguments but got " +
            arguments.size() + ".");
      }

      return function.call(this, arguments);
    }

    @Override
    public Object visitGroupingExpr(Grouping expr) {
        return evaluate(expr.expression);
    }

    @Override
    public Object visitUnaryExpr(Unary expr) {
        var right = evaluate(expr.right);
        switch(expr.operator.type)
        {
            case BANG:
                //no need to literally calculate it...
                //return(!evaluate(expr.right));
                return !isTrue(expr);
            case MINUS:
                checkNumOperand(expr.operator, right);
                return(-(double)right);
            default:
        }

        //Unreachable
        return null;
    }

    @Override
    public Object visitVariableExpr(Expr.Variable expr) {
      return environment.get(expr.name);
    }

    private void checkNumOperand(Token operator, Object operand)
    {
        if(operand instanceof Double)   return;
        else
        {
            throw new RuntimeError(operator, "Operand must be a number.");
        }
    }

    private void checkNumOperands(Token operator, Object left, Object right) 
    {
        if (left instanceof Double && right instanceof Double) return;
        
        throw new RuntimeError(operator, "Operands must be numbers.");
      }


    private Boolean isTrue(Object object)
    {   
        if((object instanceof Boolean) && (boolean)object == false || object == null)
        {
            return false;
        }
        return true;
    }

    private String stringfy(Object obj)
    {
        if(obj == null)
        {
            return "nil";
        }
        return obj.toString();
    }

    @Override
    public Void visitExpressionStmt(Expression stmt) {
        evaluate(stmt.expression);
        return null;
    }

    @Override
    public Void visitFunctionStmt(Stmt.Function stmt) {
      LoxFunction function = new LoxFunction(stmt);
      environment.define(stmt.name.lexeme, function);
      return null;
    }

    @Override
    public Void visitIfStmt(Stmt.If stmt) {
      if (isTrue(evaluate(stmt.condition))) {
        execute(new BaseStmt.BasicStmt(stmt.thenBranch));
      } else if (stmt.elseBranch != null) {
        execute(new BaseStmt.BasicStmt(stmt.elseBranch));
      }
      return null;
    }
    @Override
    public Void visitPrintStmt(Print stmt) {
        System.out.println(stringfy(evaluate(stmt.expression)));
        return null;
    }

    @Override
    public Void visitVarStmt(Stmt.Var stmt) 
    {
        Object value = null;
        if (stmt.initializer != null) {
          value = evaluate(stmt.initializer);
        }
    
        environment.define(stmt.name.lexeme, value);
        return null;
    }

    @Override
    public Void visitWhileStmt(Stmt.While stmt) 
    {
      while (isTrue(evaluate(stmt.condition))) 
      {
        execute(new BaseStmt.BasicStmt(stmt.body));
      }
      return null;
    }

      @Override
      public Object visitAssignExpr(Expr.Assign expr) {
        Object value = evaluate(expr.value);
        environment.assign(expr.name, value);
        return value;
      }

    @Override
    public Object visitEventCatExpr(EventCat expr) {
        // TODO Auto-generated method stub
        throw new UnsupportedOperationException("Unimplemented method 'visitEventCatExpr'");
    }

    @Override
    public Void visitStellaStmtBaseStmt(StellaStmt basestmt) {
        StellarisStmt stellarisStmt = basestmt.stella_stmt;
        stellarisStmt.accept(this);
        return null;
    }

    @Override
    public Void visitBasicStmtBaseStmt(BasicStmt basestmt) {
        basestmt.statement.accept(this);
        return null;
    }

    @Override
    public Void visitBlockStellarisStmt(Block stellarisstmt) {
        // TODO Auto-generated method stub
        throw new UnsupportedOperationException("Unimplemented method 'visitBlockStellarisStmt'");
    }

    @Override
    public Void visitEventStellarisStmt(Event stellarisstmt) {
        StellaEvent event = new StellaEvent(stellarisstmt);     //StellaEvent is a class for interprete, as StellarisStmt.Event is a class for parser
        //List<Stmt> stmt_list = stellarisstmt.body;

        // var domain_expr = evaluate(event.GetDomainName());
        // assert domain_expr instanceof String;
        globals.define(stellarisstmt.name.lexeme, event);
        // switch ((String) domain_expr) {
        //     case "global":
        //         evaluate(stmt_list)
        //         break;
        
        //     default:
        //         break;
        // }
        return null;
    }

    @Override
    public Void visitHandleStellarisStmt(Handle stellarisstmt) {
        // TODO Auto-generated method stub
        throw new UnsupportedOperationException("Unimplemented method 'visitHandleStellarisStmt'");
    }

    @Override
    public Void visitCallStellarisStmt(Call stellarisstmt) {
        // TODO Auto-generated method stub
        throw new UnsupportedOperationException("Unimplemented method 'visitCallStellarisStmt'");
    }

}
